Skip to content

Track authored measurement freshness - #79

Merged
max-sixty merged 6 commits into
mainfrom
codex/measured-values
Aug 27, 2026
Merged

Track authored measurement freshness#79
max-sixty merged 6 commits into
mainfrom
codex/measured-values

Conversation

@max-sixty

@max-sixty max-sixty commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a registry-declared x-measured join between an authored scalar timestamp and one live data source
  • ship an inline lf-num widget whose text freezes the stated value while version check and page state report later source runs
  • document and exercise the workflow in the default package and heat-loss example, including its companion source snapshot

Validation

  • pre-commit run --all-files
  • uv run pytest tests/test_interact_contract.py tests/test_interact_document.py (292 passed after merging current main)
  • uv run pytest tests (641 passed, 1 skipped after merging current main)
  • desktop and mobile browser sweep of examples/heat-loss.html

Closes #69

This was written by Codex on behalf of max-sixty

@leaf-agent leaf-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the merged tree at d288f56 and ran tests/test_interact_contract.py plus tests/test_interact_document.py (292 passed), ruff, and scripts/keydocs.py — the generated docs table and gallery.html/gallery.data.json are in step with the sources. The x-measured join sits at the right depth: the registry names both halves, Python reads it generically, and the follow-up commit closes the two ways a declaration could hand the frozen half to replay.

Two substantive things, then three cleanups inline.

at and updated are two different clocks. The lf-num entry says at is "the aware RFC 3339 instant when that authored value was captured", but measurement_lag_entries compares it against the source's updated, which leaf data set stamps at wall-clock when the value is written. An author following the documented order — run the measurement, leaf data set, then write the number and its capture instant — ends up with at < updated and a measurement behind its source advisory on the page they just authored. The only quiet at is one at or after that write, which is exactly what the new test does (write(updated)), and nothing in page-authoring.md says so. Suggestion inline.

examples/heat-loss.html binds heat-weather-gap and ships no heat-loss.data.json. examples/CLAUDE.md states it unconditionally: "An example that binds a widget input to a source ships <stem>.data.json... Every page builder named above sets those values through leaf data set, so binding, contract validation, revisioning, live preview, browser sweeps, and the static site all exercise the same door." Without one, the new measured-scalar contract goes through that door nowhere in the corpus, and x-measured — the reading this PR exists for — never runs on a real page; test_examples_pass_check reaches the if data_seed.exists() branch and skips it. Seeding it isn't free, and for the same clock reason as above: cmd_data_set writes now_iso(), so updated would land at build time, permanently past at="2026-02-01T12:00:00Z", and heat-loss would carry standing advice on every corpus lint. Which way to go is a judgement about what the corpus should demonstrate — but as it stands the one new contract in the layer has no example behind it.

Comment thread plugins/leaf/skills/leaf/references/page-authoring.md Outdated
Comment thread plugins/leaf/skills/leaf/scripts/leaf/data.py Outdated
Comment thread plugins/leaf/skills/leaf/scripts/leaf/checking.py Outdated
Comment thread plugins/leaf/skills/leaf/scripts/leaf/checking.py Outdated
Comment thread plugins/leaf/skills/leaf/scripts/leaf/registry.py Outdated
Comment thread plugins/leaf/skills/leaf/scripts/leaf/page.py Outdated
@max-sixty

Copy link
Copy Markdown
Owner Author

Addressed the two review-level integration points in 4705a81 as well.

  • at now unambiguously pins the data source write’s own updated instant; the authoring guide gives the required run → data-set → author order and explains the one-clock comparison.
  • examples/heat-loss.data.json now supplies heat-weather-gap as 38, and regenerated gallery.data.json composes it. The authored page remains frozen at 40 while the seeded live source represents a later run, so the corpus exercises binding, contract validation, and the freshness advisory through the real leaf data set door.

Validation on the resulting head: 330 owning tests, the normal suite at 642 passed / 1 skipped, and all pre-commit hooks.

This was written by Codex on behalf of max-sixty

@leaf-agent

Copy link
Copy Markdown
Collaborator

Read the six threads against 7005d0d — each suggestion landed as described, and aware_instant in the registry is now the only parse of the format, with is_aware_datetime reading through it rather than beside it. Both review-level points close too: the guide states the run → leaf data set → pin-at order with the reason, and examples/heat-loss.data.json puts the new contract through cmd_data_set in test_examples_pass_check, so the corpus exercises the join and shows the freshness advisory instead of arranging to avoid it — lag is advice rather than an error, so the lint stays green while demonstrating it. tests/test_interact_contract.py, tests/test_interact_document.py, and that examples check pass here (293 passed); the PR's test job is still running on this head.

One observation, not a blocker: the guide now asks the author to pin at to the write's own updated instant, but data set echoes only the revision — the instant has to be recovered afterwards from page state or from the version check advice line the pinning exists to prevent. Naming it in that echo would put the value in the same place as the instruction to use it.

@max-sixty
max-sixty merged commit 1617870 into main Aug 27, 2026
4 checks passed
@max-sixty
max-sixty deleted the codex/measured-values branch August 27, 2026 11:55
max-sixty pushed a commit that referenced this pull request Aug 27, 2026
## Problem

[Run
33069480665](https://github.com/max-sixty/leaf/actions/runs/33069480665)
on `main` did not fail a test — it stopped. The suite ran clean to `[
72%]` at 12:17:45Z and then wrote nothing for twenty-three minutes,
until the step's 45-minute bound killed it. The bound kills pytest
rather than failing it, so nothing is written on the way out, and the
default terminal reaches the log only once every 72 tests: the run named
no test at all, which is the one fact `tend-ci-fix` needed and the only
one it could not get.

## Solution

Add `-v` to the CI invocation. Under `-v` xdist writes a nodeid when a
worker picks a test up and writes it again when the test reports, so the
trailing unmatched line is exactly the test that stopped. It watches
nothing and ends nothing, so it does not reopen the decision in
`pyproject.toml`: the hang guard removed in #46 killed a worker in order
to print a suspended greenlet's frames and name no test, and `-v` names
the test while intervening in nothing. It also adds no configuration:
`pyproject.toml` is untouched and the local default is unchanged,
because the problem is CI-specific — on a workstation a wedge is visible
in the terminal, while here a killed process leaves the log as the sole
witness. No test is weakened and no bound moves.

**This does not fix the stall.** The stall is not reproducible and I
could not identify it. What it fixes is that the next one will be
answerable instead of anonymous.

<details><summary>Diagnosis: why no fix for the stall itself</summary>

**The stall was not slowness.** The failed run was ahead of a healthy
one at the same point — it reached 72% at 22m08s, where the green run on
[bf87d64](https://github.com/max-sixty/leaf/actions/runs/33069396385)
reached it at 25m16s. Then it produced nothing further. Normal blocks in
that region run 2–7 minutes, so twenty-three is 3–4× the worst of them,
and it was still silent when killed.

**Both workers stopped, not one.** Under `--dist loadgroup` a wedged
worker holds only its own batch; the other keeps draining `pending` and
the counter keeps moving. It did not move at all. The runner's orphan
sweep found both worker processes and both `chrome-headless-shell`
processes still alive at the kill, so nothing had crashed either.

**The code is not implicated.** The same tree passed on its own PR
branch ([run
33066712190](https://github.com/max-sixty/leaf/actions/runs/33066712190),
35m03s), and the two commits that followed it onto `main` —
[cbff468](https://github.com/max-sixty/leaf/actions/runs/33072090225)
and
[03a0f31](https://github.com/max-sixty/leaf/actions/runs/33073047974) —
are both green with #79's code in them. `main` is not currently red.

**It does not reproduce.** I ran the complete nightly suite on `main` on
an `ubuntu-24.04` runner of the same class: `1381 passed, 6 skipped in
2296.13s (0:38:16)`, no failures and no stall, including the whole
`test_render_gate.py` / `test_render_navigation.py` region that 72%
falls in (`test_render_gate.py` spans items 944–1016 of 1387). That run
was under `-v`, which is also where the claim above was checked rather
than assumed: 1387 nodeid lines against 1381 `PASSED` lines, and each
poll of the growing log ended on the bare nodeid of whatever was in
flight — the 283s ring sweep stood alone for 171 lines. Output to a
pipe, as in Actions, not a terminal.

**The region is not the untimed-launcher class either.** #46 left one
shape unbounded — the cold-cache runs in `test_interact_layer.py` that
let `uv` resolve through the index with no `timeout=` — and it is not
this: those are items 336–463, long finished by 72%, and everything in
the stall's region goes through `render_version` and the browser, where
Playwright's own 30-second bounds apply.

**No upstream incident covers it.** githubstatus lists nothing against
Actions in the 12:17–12:41Z window; the only incident open that morning
was Copilot AI Model Providers, resolved 12:12:58Z. So this is not the
dispatch-layer shape of #47, and it is not the stale-`page.route`
cascade of #34 either.

**It is the first wedge of its kind.** Across all 54 `ci` failures on
`main` with a recorded test step, exactly two spent the full 45 minutes.
The other,
[32954830645](https://github.com/max-sixty/leaf/actions/runs/32954830645),
is the one #46 answered: it reached 99%, the old faulthandler guard
fired at three minutes, printed the server thread, execnet's reader and
Playwright's event loop, killed `gw1` (`node down: Not properly
terminated`) and left the controller on a session that never ended. That
was the guard's own wedge and the guard is gone. Every other failure
concluded in under 40 minutes on a real assertion.

That leaves this one unclassified rather than transient, and
`running-tend` says to treat unclassified as durable. A durable cause I
cannot name has no safe fix, so the change above is scoped to making the
next occurrence nameable rather than guessing at the wedge.

</details>

<details><summary>Separately: the bound's headroom is thinning
fast</summary>

Not addressed here — whether the suite should grow or the bound should
is not a call to make on one occurrence — but worth seeing. Test-step
wall time on `main`, oldest to newest:

| Run started (UTC) | Items | Test step |
| --- | --- | --- |
| 08-26 09:48 | 1230 | 17.6 min to 99% |
| 08-26 18:58 | — | 18.2 min |
| 08-26 20:47 | — | 19.9 min |
| 08-26 21:04 | — | 24.3 min |
| 08-27 00:24 | — | 24.9 min |
| 08-27 03:51 | — | 26.4 min |
| 08-27 06:31 | — | 36.7 min |
| 08-27 11:53 | 1381 | 36.0 min |
| 08-27 12:41 | — | 35.4 min |

The bound is 45. Note that the item count rose 13% while the time rose
110%, so this is per-test cost rather than test count. Four tests carry
13 of the roughly 76 worker-minutes, all corpus sweeps and three of them
on `gallery`:

```
283.65s  test_render_controls.py::test_every_ring_the_layer_draws_is_shown_whole_somewhere_in_the_corpus
245.85s  test_render_controls.py::test_a_press_leaves_its_neighbours_where_they_were[gallery]
140.72s  test_render_controls.py::test_an_aimed_press_does_only_what_the_outline_promised[gallery]
109.79s  test_render_anchors.py::test_every_passage_in_a_real_page_can_be_quoted[gallery]
```

`-n 2` is also the whole machine's allowance on a 4-vCPU runner, where
the cap's stated reason — "keep overlapping browser test suites from
saturating the machine"
([1ee98fe](1ee98fe))
— is a workstation's reason, and in CI no second suite exists. I have
not touched it: more workers is more browser load on exactly the shape
of race `running-tend` says dominates red runs here, and that trade is
not mine to make.

</details>

<details><summary>Cost of `-v`</summary>

About 2,800 lines and ~250 KB of log per run, in place of ~20 progress
lines. The visible cost is to `gh run view --log-failed` on an ordinary
red run, which now carries the roster ahead of the traceback.

</details>

---

Automated fix for [failed run
33069480665](https://github.com/max-sixty/leaf/actions/runs/33069480665)

Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A measured figure in a version is frozen prose, and nothing ties it to the run that produced it

2 participants